﻿using Microsoft.Owin.Security;
using Sustainalytics.LongRunningTaskManager.Interface;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Sustainalytics.LongRunningTaskManager
{
    /// <summary>
    /// Task manager for managing the Long Running Tasks.
    /// </summary>
    public class LongRunningTaskManager : ILongRunningTaskManager
    {
        // task manager settings
        private readonly LongRunningTaskManagerSettings _settings;

        // main cancellation token source which controlls all task manager tasks cancellation
        private readonly CancellationTokenSource _mainCancellationTokenSource = new CancellationTokenSource();

        // factory used for creating new tasks
        private readonly ILongRunningTaskFactory _taskFactory;

        // currently running tasks
        private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, LongRunningTaskInstance>> _tasks = 
            new ConcurrentDictionary<string, ConcurrentDictionary<string, LongRunningTaskInstance>>();

        // task instance snapshot repository
        private readonly ILongRunningTaskInstanceSnapshotRepository _taskSnapshotRepo;

        // task semaphore for controlling the number of running tasks
        private readonly SemaphoreSlim _tasksSem;

        // whether current manager is stopping and cannot accept any requests
        private bool _isStopping = false;

        // object used to sync starting new tasks - required in case tasks are started concurrently by the same user
        private object _startTaskSync = new object();

        // task result transformer
        private readonly ILongRunningTaskResultTransformer _taskResultTransformer;

        // task instance queue of pending task instances without an upper bound
        private readonly BlockingCollection<LongRunningTaskInstance> _taskInstanceQueue =
            new BlockingCollection<LongRunningTaskInstance>(new ConcurrentQueue<LongRunningTaskInstance>());

        // task that watch for task instance queue
        private Task _watchQueueTask;


        /// <summary>
        /// Main cancellation token.
        /// </summary>
        public CancellationToken MainCancellationToken
        {
            get
            {
                return this._mainCancellationTokenSource.Token;
            }
        }


        /// <summary>
        /// Task manager settings.
        /// </summary>
        public ILongRunningTaskManagerSettings Settings { get { return _settings; } }

        /// <summary>
        /// Current task factory.
        /// </summary>
        public ILongRunningTaskFactory TaskFactory { get { return _taskFactory; } }   



        /// <summary>
        /// Ctor.
        /// </summary>
        public LongRunningTaskManager(
            ILongRunningTaskInstanceSnapshotRepository taskSnapshotRepo,
            ILongRunningTaskResultTransformer taskResultTransformer)
        {
            try
            {
                LongRunningTaskManagerEventSource.Log.TaskManagerInit();

                this._settings = LongRunningTaskManagerSettings.FromConfig();
                this._taskFactory = new LongRunningTaskFactory();
                this._taskSnapshotRepo = taskSnapshotRepo;
                this._tasksSem = new SemaphoreSlim(this._settings.MaxOverallManagedTasks);
                this._taskResultTransformer = taskResultTransformer;

                Init();

                LongRunningTaskManagerEventSource.Log.TaskManagerInitSuccess();
            }
            catch (Exception ex)
            {
                LongRunningTaskManagerEventSource.Log.TaskManagerInitError(ex);

                throw ;
            }
        }

        /// <summary>
        /// Ctor.
        /// </summary>
        /// <param name="settings"> Task manager settings. </param>
        /// <param name="taskFactory"> Task factory. </param>
        public LongRunningTaskManager(
            LongRunningTaskManagerSettings settings,
            ILongRunningTaskFactory taskFactory,
            ILongRunningTaskInstanceSnapshotRepository taskSnapshotRepo,
            ILongRunningTaskResultTransformer taskResultTransformer)
        {
            try
            {
                LongRunningTaskManagerEventSource.Log.TaskManagerInit();

                this._settings = settings;
                this._taskFactory = taskFactory;
                this._taskSnapshotRepo = taskSnapshotRepo;
                this._tasksSem = new SemaphoreSlim(this._settings.MaxOverallManagedTasks);
                this._taskResultTransformer = taskResultTransformer;

                Init();

                LongRunningTaskManagerEventSource.Log.TaskManagerInitSuccess();
            }
            catch (Exception ex)
            {
                LongRunningTaskManagerEventSource.Log.TaskManagerInitError(ex);

                throw;
            }
        }

        /// <summary>
        /// Initialize task manager.
        /// </summary>
        private void Init()
        {
            // start watching task instance queue
            StartWatchingTaskInstanceQueue();

            // reload tasks saved on a previous instance
            ReloadTasks();
        }

        /// <summary>
        /// Reload tasks that were saved before the moment the Task Manager stopped or failed.
        /// </summary>
        private void ReloadTasks()
        {
            // get pending tasks
            IEnumerable<LongRunningTaskInstanceSnapshot> taskInstanceSnapshotLst = _taskSnapshotRepo.GetAll();

            // re-add task instance from snapshot
            foreach (LongRunningTaskInstanceSnapshot taskInstanceSnapshot in taskInstanceSnapshotLst)
            {
                // create task from snapshot
                LongRunningTaskInstance taskInstance = CreateTaskInstanceFromSnapshot(null, taskInstanceSnapshot);

                // add task instance to dictionary
                AddTaskInstanceToDictionary(taskInstanceSnapshot.Username, taskInstance);
            }
        }

        /// <summary>
        /// Start watching task instance queue.
        /// </summary>
        private Task StartWatchingTaskInstanceQueue()
        {
            _watchQueueTask = Task.Factory.StartNew(() =>
                {
                    while (true)
                    {
                        // check semaphore
                        _tasksSem.Wait(this.MainCancellationToken);

                        // get next task instance
                        var taskInstance = _taskInstanceQueue.Take(this.MainCancellationToken);

                        // unrelease semaphore when the task completes
                        taskInstance.ManagedTask.ContinueWith(prevTask =>
                            {
                                _tasksSem.Release();
                            }, MainCancellationToken);

                        // start task instance
                        taskInstance.Start();
                    }
                }, 
                this.MainCancellationToken);

            return _watchQueueTask;
        }


        /// <summary>
        /// Checks whether max number of users has exceeded.
        /// </summary>
        public void CheckMaxTasksPerUserReachedException(string username)
        {
            // get user dictionary of task instances
            var userTaskInstances = this.GetUserTaskInstancesDictionary(username);

            // check if the user has reached the max ammount of tasks
            if (userTaskInstances != null && userTaskInstances.Count >= this._settings.MaxTasksPerUser)
            {
                throw new LongRunningMaxTasksPerUserReachedException();
            }
        }

        /// <summary>
        ///  Start task instance.
        /// </summary>
        /// <param name="ticket"> Authentication ticket. </param>
        /// <param name="title"> Task title. </param>
        /// <param name="paramJson"> Parameter (JSON) that will be passed to task creation. </param>
        /// <param name="taskType"> Task type to be instantiated. </param>
        /// <returns> Task instance ID. </returns>
        public string Start(string id, string title, AuthenticationTicket ticket, string paramJson, LongRunningTaskTypeEnum taskType, bool isUpload, bool isDownload, long? order)
        {
            string username = ticket == null ? null : ticket.Identity.Name;

            try
            {
                ThrowIfStopping();

                if (ticket == null)
                {
                    throw new LongRunningTaskManagerAuthenticationTicketNullException();
                }
                
                LongRunningTaskManagerEventSource.Log.TaskManagerStartTask(title, username, paramJson, taskType);


                LongRunningTaskInstance taskInstance;

                lock (_startTaskSync)
                {
                    CheckMaxTasksPerUserReachedException(username);

                    // create task instance
                    taskInstance = CreateNewTaskInstance(id, title, ticket, paramJson, taskType, order, isUpload, isDownload);
                }

                // create task instance snapshot
                LongRunningTaskInstanceSnapshot taskInstanceSnapshot = new LongRunningTaskInstanceSnapshot(taskInstance);

                // add task instance snapshot to repository
                this._taskSnapshotRepo.Insert(taskInstanceSnapshot);

                // add task to dictionary
                AddTaskInstanceToDictionary(ticket.Identity.Name, taskInstance);

                // after the task is finished update task instance snapshot into repository
                taskInstance.ManagedTask.ContinueWith(prevTask =>
                {
                    // create task instance snapshot
                    LongRunningTaskInstanceSnapshot taskInstanceSnapshotUpdate = new LongRunningTaskInstanceSnapshot(taskInstance);

                    // save task instance into repository
                    this._taskSnapshotRepo.Update(taskInstanceSnapshotUpdate);
                }, MainCancellationToken);

                // push task to queue
                _taskInstanceQueue.Add(taskInstance, this.MainCancellationToken);

                LongRunningTaskManagerEventSource.Log.TaskManagerStartTaskSuccess(title, username, paramJson, taskType);

                return taskInstance.Id;
            }
            catch (Exception ex)
            {
                LongRunningTaskManagerEventSource.Log.TaskManagerStartTaskError(title, username, paramJson, taskType, ex);

                throw;
            }
        }

        /// <summary>
        /// Get overall status of current task instances running for a specific username.
        /// </summary>
        /// <param name="ticket"> Authentication ticket. </param>
        /// <returns>  </returns>
        public List<LongRunningTaskInstanceStatus> GetOverallStatus(AuthenticationTicket ticket)
        {
            string username = ticket == null ? null : ticket.Identity.Name;

            try
            {
                ThrowIfStopping();

                if (ticket == null)
                {
                    throw new LongRunningTaskManagerAuthenticationTicketNullException();
                }

                // get user instances
                List<LongRunningTaskInstance> taskInstances = GetUserTaskInstances(ticket.Identity.Name);

                // get task instances statuses
                List<LongRunningTaskInstanceStatus> taskInstancesStatuses = taskInstances.Select(ti => ti.Status).ToList();

                return taskInstancesStatuses;
            }
            catch (Exception ex)
            {
                LongRunningTaskManagerEventSource.Log.TaskManagerGetOverallStatusError(username, ex);

                throw;
            }
        }

        /// <summary>
        /// Get long running task status.
        /// </summary>
        /// <param name="ticket"> Authentication ticket. </param>
        /// <param name="taskInstanceId"> Task instance ID. </param>
        /// <returns> Task instance status. </returns>
        public LongRunningTaskInstanceStatus GetStatus(AuthenticationTicket ticket, string taskInstanceId)
        {
            string username = ticket == null ? null : ticket.Identity.Name;

            try
            {
                ThrowIfStopping();

                if (ticket == null)
                {
                    throw new LongRunningTaskManagerAuthenticationTicketNullException();
                }

                if (string.IsNullOrEmpty(taskInstanceId))
                {
                    throw new LongRunningTaskInstanceIdNullOrEmptyException();
                }

                LongRunningTaskManagerEventSource.Log.TaskManagerGetStatus(username, taskInstanceId);

                // get user task instance
                LongRunningTaskInstance taskInstance = GetUserTaskInstance(ticket.Identity.Name, taskInstanceId);

                if (taskInstance == null)
                {
                    throw new LongRunningTaskInstanceNotFoundException();
                }

                LongRunningTaskManagerEventSource.Log.TaskManagerGetStatusSuccess(username, taskInstanceId);

                return taskInstance.Status;
            }
            catch (Exception ex)
            {
                LongRunningTaskManagerEventSource.Log.TaskManagerGetStatusError(username, taskInstanceId, ex);

                throw;
            }
        }

        /// <summary>
        /// Get task instance result.
        /// </summary>
        /// <param name="ticket"> Authentication ticket. </param>
        /// <param name="taskInstanceId"> Task instance ID. </param>
        /// <returns> Task instance result. </returns>
        public ILongRunningTaskResult GetResult(AuthenticationTicket ticket, string taskInstanceId)
        {
            string username = ticket == null ? null : ticket.Identity.Name;

            try
            {
                ThrowIfStopping();

                if (ticket == null)
                {
                    throw new LongRunningTaskManagerAuthenticationTicketNullException();
                }

                if (string.IsNullOrEmpty(taskInstanceId))
                {
                    throw new LongRunningTaskInstanceIdNullOrEmptyException();
                }

                LongRunningTaskManagerEventSource.Log.TaskManagerGetResult(username, taskInstanceId);

                // get user task instance
                LongRunningTaskInstance taskInstance = GetUserTaskInstance(ticket.Identity.Name, taskInstanceId);

                if (taskInstance == null)
                {
                    throw new LongRunningTaskInstanceNotFoundException();
                }

                LongRunningTaskManagerEventSource.Log.TaskManagerGetResultSuccess(username, taskInstanceId);

                return taskInstance.Result;
            }
            catch (Exception ex)
            {
                LongRunningTaskManagerEventSource.Log.TaskManagerGetResultError(username, taskInstanceId, ex);

                throw;
            }
        }

        /// <summary>
        /// Cancel a specific task instance.
        /// </summary>
        /// <param name="ticket"> Authentication ticket. </param>
        /// <param name="taskInstanceId"> Task instance ID. </param>
        /// <returns> Managed Task which cancells the task instance. </returns>
        public Task Cancel(AuthenticationTicket ticket, string taskInstanceId)
        {
            string username = ticket == null ? null : ticket.Identity.Name;

            try
            {
                ThrowIfStopping();

                if (ticket == null)
                {
                    throw new LongRunningTaskManagerAuthenticationTicketNullException();
                }

                if (string.IsNullOrEmpty(taskInstanceId))
                {
                    throw new LongRunningTaskInstanceIdNullOrEmptyException();
                }

                LongRunningTaskManagerEventSource.Log.TaskManagerCancel(username, taskInstanceId);

                // get task instance
                LongRunningTaskInstance taskInstance = GetUserTaskInstance(ticket.Identity.Name, taskInstanceId);

                if (taskInstance == null)
                {
                    //throw new LongRunningTaskInstanceNotFoundException();
                    //if instance not found, it might be because the user spammed the cancel button
                    return null;
                }

                // cancel instance
                taskInstance.Cancel();

                LongRunningTaskManagerEventSource.Log.TaskManagerCancelSuccess(username, taskInstanceId);

                return taskInstance.ManagedTask;
            }
            catch (Exception ex)
            {
                LongRunningTaskManagerEventSource.Log.TaskManagerCancelError(username, taskInstanceId, ex);

                throw;
            }
        }

        public void CancelAndWait(AuthenticationTicket ticket, string taskInstanceId)
        {
            SkipCancellations(() =>
                {
                    this.Cancel(ticket, taskInstanceId).Wait(MainCancellationToken);
                });
        }

        /// <summary>
        /// Cancel and remove requested task instance.
        /// </summary>
        /// <param name="ticket"> Authentication ticket. </param>
        /// <param name="taskInstanceId"> Task instance ID. </param>
        /// <returns> Managed task which cancels and then removes the task instance. </returns>
        public Task Remove(AuthenticationTicket ticket, string taskInstanceId)
        {
            string username = ticket == null ? null : ticket.Identity.Name;

            try
            {
                ThrowIfStopping();

                if (ticket == null)
                {
                    throw new LongRunningTaskManagerAuthenticationTicketNullException();
                }

                if (string.IsNullOrEmpty(taskInstanceId))
                {
                    throw new LongRunningTaskInstanceIdNullOrEmptyException();
                }

                LongRunningTaskManagerEventSource.Log.TaskManagerRemove(username, taskInstanceId);

                // after the instance has been cancelled remove the instance from dictionary
                var cancelTask = this.Cancel(ticket, taskInstanceId).ContinueWith(managedTask =>
                {
                    // remove task instance from repository
                    this._taskSnapshotRepo.Delete(taskInstanceId);

                    // get user task instances dictionary
                    ConcurrentDictionary<string, LongRunningTaskInstance> userTaskInstanceDic = GetUserTaskInstancesDictionary(username);

                    // task instace being removed
                    LongRunningTaskInstance taskInstanceRemoved;

                    // remove current task instance and throw exception only if exists and cannot be removed
                    if (userTaskInstanceDic != null && userTaskInstanceDic.ContainsKey(taskInstanceId) && userTaskInstanceDic.TryRemove(taskInstanceId, out taskInstanceRemoved) == false)
                    {
                        throw new LongRunningTaskInstanceDictionaryRemoveException();
                    }
                }, MainCancellationToken);

                LongRunningTaskManagerEventSource.Log.TaskManagerRemoveSuccess(username, taskInstanceId);

                return cancelTask;
            }
            catch (Exception ex)
            {
                LongRunningTaskManagerEventSource.Log.TaskManagerRemoveError(username, taskInstanceId, ex);

                throw;
            }
        }

        /// <summary>
        /// Get task async info.
        /// </summary>
        /// <param name="ticket"> Authentication ticket. </param>
        /// <param name="taskInstanceId"> Task instance ID. </param>
        /// <returns> Managed task. </returns>
        public ILongRunningTaskInstanceAsyncInfo GetTaskAsyncInfo(AuthenticationTicket ticket, string taskInstanceId)
        {
            string username = ticket == null ? null : ticket.Identity.Name;

            try
            {
                ThrowIfStopping();

                if (ticket == null)
                {
                    throw new LongRunningTaskManagerAuthenticationTicketNullException();
                }

                if (string.IsNullOrEmpty(taskInstanceId))
                {
                    throw new LongRunningTaskInstanceIdNullOrEmptyException();
                }

                // get user task instance
                LongRunningTaskInstance taskInstance = GetUserTaskInstance(ticket.Identity.Name, taskInstanceId);

                if (taskInstance == null)
                {
                    throw new LongRunningTaskInstanceNotFoundException();
                }

                LongRunningTaskManagerEventSource.Log.TaskManagerGetResultSuccess(username, taskInstanceId);

                return taskInstance.AsyncInfo;
            }
            catch (Exception ex)
            {
                LongRunningTaskManagerEventSource.Log.TaskManagerGetResultError(username, taskInstanceId, ex);

                throw;
            }
        }

        /// <summary>
        /// Stops current manager.
        /// </summary>
        /// <returns> Task that stops the current manager. </returns>
        public Task StopManager()
        {
            try
            {
                LongRunningTaskManagerEventSource.Log.TaskManagerStopping();

                ThrowIfStopping();

                // mark that task manager is stopping
                this._isStopping = true;

                // cancel all tasks
                this._mainCancellationTokenSource.Cancel();

                // get all task instances managed tasks
                Task<ILongRunningTaskResult>[] taskInstancesTasks = this._tasks.SelectMany(ud => ud.Value.Select(td => td.Value.ManagedTask)).ToArray();
                

                var taskStopped =

                    // wait for all tasks to stop
                    Task.Factory.StartNew(() =>
                        {
                            var tasksToWait = taskInstancesTasks.Select(t => (Task)t).ToList();
                            tasksToWait.Add(this._watchQueueTask);

                            Task.WaitAll(tasksToWait.ToArray());
                        }, MainCancellationToken)

                    // log when task manager was stopped
                    .ContinueWith(t => { LongRunningTaskManagerEventSource.Log.TaskManagerStopped(); }, MainCancellationToken);

                LongRunningTaskManagerEventSource.Log.TaskManagerStoppingSuccess();

                return taskStopped;
            }
            catch (Exception ex)
            {
                LongRunningTaskManagerEventSource.Log.TaskManagerStoppingError(ex);

                throw;
            }
        }

        /// <summary>
        /// Stop manager and wait.
        /// </summary>
        public void StopManagerAndWait()
        {
            SkipCancellations(() =>
                {
                    this.StopManager().Wait(MainCancellationToken);
                });
        }


        /// <summary>
        /// Create new task instance.
        /// </summary>
        /// <param name="title"> Task title. </param>
        /// <param name="ticket"> Authentication ticket. </param>
        /// <param name="paramJson"> Task parameter as JSON. </param>
        /// <param name="taskType"> Task type. </param>
        /// <param name="order"></param>
        /// <param name="isUpload"></param>
        /// <param name="isDownload"></param>
        /// <returns> Task instance. </returns>
        private LongRunningTaskInstance CreateNewTaskInstance(
            string id,
            string title, 
            AuthenticationTicket ticket, 
            string paramJson, 
            LongRunningTaskTypeEnum taskType,
            long? order,
            bool isUpload,
            bool isDownload)
        {
            // task instance ID
            id = id ?? Guid.NewGuid().ToString();

            // create task instance
            var taskInstance = new LongRunningTaskInstance(this.TaskFactory, ticket, id, title,
                paramJson, taskType, this._taskResultTransformer, this.MainCancellationToken, order, isUpload,
                isDownload);

            return taskInstance;
        }

        /// <summary>
        /// Create 
        /// </summary>
        /// <param name="ticket"></param>
        /// <param name="taskInstanceSnapshot"> Task instance snapshot. </param>
        /// <returns> Task instance. </returns>
        private LongRunningTaskInstance CreateTaskInstanceFromSnapshot(AuthenticationTicket ticket, LongRunningTaskInstanceSnapshot taskInstanceSnapshot)
        {
            // task result transformer
            ILongRunningTaskResultTransformer taskResTransformer = this._taskResultTransformer;

            // task instance
            LongRunningTaskInstance taskInstance = new LongRunningTaskInstance(
                taskFactory: this.TaskFactory,
                taskInstanceSnapshot: taskInstanceSnapshot,
                ticket: ticket,
                taskResultTransformer: taskResTransformer,
                parentCancellationToken: this.MainCancellationToken);

            return taskInstance;
        }

        /// <summary>
        /// Add task instance to dictionary.
        /// </summary>
        /// <param name="username"></param>
        /// <param name="taskInstance"> Task instance. </param>
        private void AddTaskInstanceToDictionary(string username, LongRunningTaskInstance taskInstance)
        {
            ConcurrentDictionary<string, LongRunningTaskInstance> userTasksInstances = GetUserTaskInstancesDictionary(username);

            if (userTasksInstances == null)
            {
                // create user dictionary
                userTasksInstances = new ConcurrentDictionary<string, LongRunningTaskInstance>();

                // add user dictionary
                if (_tasks.TryAdd(username, userTasksInstances) == false)
                {
                    throw new LongRunningTaskInstanceDictionaryAddException();
                }
            }

            // add task instance
            userTasksInstances.TryAdd(taskInstance.Id, taskInstance);
        }

        /// <summary>
        /// Get user task instances dictionary.
        /// </summary>
        /// <param name="username"> Username. </param>
        /// <returns> Task instances dictionary. </returns>
        private ConcurrentDictionary<string, LongRunningTaskInstance> GetUserTaskInstancesDictionary(string username)
        {
            // user task instance dictionary
            ConcurrentDictionary<string, LongRunningTaskInstance> userTaskInstances;

            // get user dictionary
            if (this._tasks.TryGetValue(username, out userTaskInstances) == false)
            {
                return null;
            }

            return userTaskInstances;
        }

        /// <summary>
        /// Get user task instance.
        /// </summary>
        /// <param name="username"> Username. </param>
        /// <param name="taskInstanceId"> Task instance ID. </param>
        /// <returns> Task instance. </returns>
        private LongRunningTaskInstance GetUserTaskInstance(string username, string taskInstanceId)
        {
            // user task instance dictionary
            ConcurrentDictionary<string, LongRunningTaskInstance> userTaskInstances = GetUserTaskInstancesDictionary(username);

            if (userTaskInstances == null)
            {
                return null;
            }

            // user task instance
            LongRunningTaskInstance taskInstance;

            // get user instance
            if (userTaskInstances.TryGetValue(taskInstanceId, out taskInstance) == false)
            {
                return null;
            }

            return taskInstance;
        }

        /// <summary>
        /// Get user task instances.
        /// </summary>
        /// <returns> User task instances. </returns>
        private List<LongRunningTaskInstance> GetUserTaskInstances(string username)
        {
            // user task instance dictionary
            ConcurrentDictionary<string, LongRunningTaskInstance> userTaskInstances = GetUserTaskInstancesDictionary(username);

            if (userTaskInstances == null)
            {
                return Enumerable.Empty<LongRunningTaskInstance>().ToList();
            }

            return userTaskInstances.Select(el => el.Value).ToList();
        }

        /// <summary>
        /// Throw if Task Manager is stopping.
        /// </summary>
        private void ThrowIfStopping()
        {
            if (this._isStopping)
            {
                throw new LongRunningTaskManagerStoppingException();
            }
        }

        private void SkipCancellations(Action action)
        {
            try
            {
                action();
            }
            catch (OperationCanceledException)
            {
                // cancellation
            }
            catch (AggregateException ex)
            {
                if (ex.InnerExceptions.Any(e => e is OperationCanceledException))
                {
                    // cancellation
                }
                else
                {
                    throw;
                }
            }
        }
    }
}
